#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
import enum
import ipaddress
import urllib.parse
from ._util import HelperWildcard


class CachePolicy(enum.Enum):
    DEFAULT = "default"
    IGNORE_AGING = "ignore-aging"
    AUTO_REFRESH = "auto-refresh"


class BackendType(enum.Enum):
    LOCAL_FILE = "local-file"
    LOCAL_DIR = "local-dir"
    HTTP_DIR = "http-dir"
    MIRRORS_API = "mirrors-api"
    ZEROCONF_MIRRORS_API = "zeroconf-mirrors-api"


class Const:
    SYSTEM_CFG_DIR = "/etc/mrget"
    USER_CFG_DIR = os.path.expanduser("~/.config/mrget")
    CFG_FILENAME = "mrget.conf"

    SYSTEM_CACHE_DIR = "/var/cache/mrget"
    USER_CACHE_DIR = os.path.expanduser("~/.cache/mrget")
    CACHE_FILENAME = "cache.sqlite"
    SCORE_FILENAME = "score.sqlite"

    @classmethod
    def getCfgDir(cls, bSysOrUser):
        if bSysOrUser:
            return cls.SYSTEM_CFG_DIR
        else:
            return cls.USER_CFG_DIR

    @classmethod
    def getCfgFileFullfn(cls, bSysOrUser):
        return os.path.join(cls.getCfgDir(bSysOrUser), cls.CFG_FILENAME)

    @classmethod
    def getCacheDir(cls, bSysOrUser):
        if bSysOrUser:
            return cls.SYSTEM_CACHE_DIR
        else:
            return cls.USER_CACHE_DIR

    @classmethod
    def getCacheFileFullfn(cls, bSysOrUser):
        return os.path.join(cls.getCacheDir(bSysOrUser), cls.CACHE_FILENAME)


class SiteUtil:

    @staticmethod
    def check_and_fill_object(obj, bSourceOrMirror):
        # property: url, host (non-optional)
        while True:
            if "url" in obj:
                if "host" in obj:
                    raise ValueError('both "url" property (%s) and "host" property (%s) exist' % (obj["url"], obj["host"]))

                try:
                    urllib.parse.urlparse(obj["url"])
                except urllib.parse.ParseError:
                    raise ValueError('invalid "url" property (%s)' % (obj["url"]))

                idstr = "(url = %s)" % (obj["url"])

                break

            if "host" in obj:
                if "url" in obj:
                    raise ValueError('both "url" (%s) and "host" (%s) property exist' % (obj["url"], obj["host"]))

                m = re.fullmatch(r'(\S+)(:\d)?', obj["host"])
                if m is None:
                    raise ValueError('invalid "host" property (%s)' % (obj["host"]))

                idstr = "(host = %s)" % (obj["host"])

                try:
                    ipaddress.IPv4Address(m.group(1))
                    if "address-families" in obj:
                        if obj["address-families"] != ["ip"]:
                            raise ValueError('invalid "address-families" property %s' % (idstr))
                    else:
                        obj["address-families"] = ["ip"]
                    break
                except ipaddress.AddressValueError:
                    pass

                try:
                    ipaddress.IPv6Address(m.group(1))
                    if "address-families" in obj:
                        if obj["address-families"] != ["ip6"]:
                            raise ValueError('invalid "address-families" property %s' % (idstr))
                    else:
                        obj["address-families"] = ["ip6"]
                    break
                except ipaddress.AddressValueError:
                    pass

            raise ValueError('no "url" or "host" property')

        # property: role (non-optional)
        if bSourceOrMirror:
            obj["role"] = "source"
        else:
            e = ValueError('invalid "role" property %s' % (idstr))
            if "role" in obj:
                if obj["role"] not in ["official-mirror", "wild-mirror", "local-mirror"]:
                    raise e
            else:
                raise e

        # property: fallback-mirror (exists and only exists when current object represents a mirror but source)
        if bSourceOrMirror:
            if "fallback-mirror" in obj:
                raise ValueError('redundant property "fallback-mirror" exist %s' % (idstr))
        else:
            if "fallback-mirror" in obj:
                if not isinstance(obj["fallback-mirror"], bool):
                    raise ValueError('invalid "fallback-mirror" property %s' % (idstr))
            else:
                obj["fallback-mirror"] = False

        # property: address-families (auto-fill)
        if "address-families" in obj:
            if not (obj["address-families"] == ["ip"] or obj["address-families"] == ["ip6"] or obj["address-families"] == ["ip", "ip6"]):
                raise ValueError('invalid "address-families" property %s' % (idstr))
        else:
            obj["address-families"] = ["ip", "ip6"]

        # property: protocol (auto-fill)
        if "url" in obj:
            if "prototocol" in obj:
                if obj["protocol"] != obj["url"].split("://")[0]:
                    raise ValueError('invalid "protocol" property %s' % (idstr))
            else:
                obj["protocol"] = obj["url"].split("://")[0]
        elif "host" in obj:
            if "prototocol" in obj:
                raise ValueError('redundant "protocol" property %s' % (idstr))
        else:
            assert False

        # property: name (optional)
        if "name" in obj:
            if not isinstance(obj["name"], str):
                raise ValueError('invalid "name" property %s' % (idstr))

        # property: description (optional)
        if "description" in obj:
            if not isinstance(obj["description"], str):
                raise ValueError('invalid "description" property %s' % (idstr))

        # property: country-code (optional)
        if "country-code" in obj:
            if not isinstance(obj["country-code"], str):
                raise ValueError('invalid "country-code" property %s' % (idstr))

        # property: partial-mirror (exists and only exists when current object represents a mirror but source)
        if bSourceOrMirror:
            if "partial-mirror" in obj:
                raise ValueError('redundant property "partial-mirror" exist %s' % (idstr))
        else:
            if "partial-mirror" in obj:
                e = ValueError('invalid "partial-mirror" property %s' % (idstr))
                if "url" in obj:
                    if not HelperWildcard.check_patterns(obj["partial-mirror"]):
                        raise e
                elif "host" in obj:
                    if not isinstance(obj["partial-mirror"], bool):
                        raise e
                else:
                    assert False
            else:
                obj["partial-mirror"] = None

        # property: readonly-mirror (exists and only exists when current object represents a mirror but source)
        if bSourceOrMirror:
            if "readonly-mirror" in obj:
                raise ValueError('redundant property "readonly-mirror" exist %s' % (idstr))
        else:
            if "readonly-mirror" in obj:
                if not isinstance(obj["readonly-mirror"], bool):
                    raise ValueError('invalid "readonly-mirror" property %s' % (idstr))
            else:
                obj["readonly-mirror"] = False

        # property: chroot (exists and only exists when current object represents a mirror but source)
        if bSourceOrMirror or ("url" not in obj) or not obj["partial-mirror"]:
            if "chroot" in obj:
                raise ValueError('redundant property "chroot" exist %s' % (idstr))
        else:
            if "chroot" in obj:
                e = ValueError('invalid "chroot" property %s' % (idstr))
                if not isinstance(obj["chroot"], str):
                    raise e
                if not obj["chroot"].startswith("/"):
                    raise e                           # only allow absolute path
                if os.path.normpath(obj["chroot"]) != obj["chroot"]:
                    raise e                           # only allow normalized path
                if not all([x == obj["chroot"] or x.startswith(obj["chroot"] + "/") for x in obj["partial-mirror"]]):
                    raise e

        # property: score (must not exist)
        if "score" in obj:
            # this function checks object received from outside, "score" is only to be measured by ourself
            del obj["score"]

    @staticmethod
    def is_same_object(obj1, obj2):
        return obj1.get("url", None) == obj2.get("url", None) and obj1.get("host", None) == obj2.get("host", None)

    @classmethod
    def is_same_site(cls, obj1, obj2):
        if "name" in obj1 and "name" in obj2 and obj1["name"] == obj2["name"]:
            if "url" in obj1 and "url" in obj2:
                return True
            if "host" in obj1 and "host" in obj2:
                return True
            return False

        return cls.is_same_object(obj1, obj2)

    @staticmethod
    def find_object_in_list(obj, obj_list):
        for i in obj_list:
            if SiteUtil.is_same_object(obj, i):
                return True
        return None
